SPB Git
2.9 KB · 43 lines typescript
1import { guarded, json } from "@/lib/api";2import { sql } from "@/lib/db";3import { Document, Packer, Paragraph, HeadingLevel, TextRun, ExternalHyperlink } from "docx";45export const GET = guarded<{ id: string }>(async (req, { params }) => {6  const r = (await sql`SELECT * FROM research_runs WHERE id = ${params.id}`)[0];7  if (!r) return json({ error: "Introuvable" }, { status: 404 });8  const fmt = req.nextUrl.searchParams.get("format");9  const safe = String(r.question).replace(/[^\w àâçéèêëîïôûùüÿœ-]/gi, "_").slice(0, 50);10  if (fmt === "md") return new Response(`# ${r.question}\n\n_Rapport de recherche — ${r.model_id} — ${new Date(r.created_at).toLocaleString("fr-CA")}_\n\n${r.report_md ?? ""}`, { headers: { "content-type": "text/markdown; charset=utf-8", "content-disposition": `attachment; filename="${safe}.md"` } });11  if (fmt === "docx") {12    const paras: Paragraph[] = [new Paragraph({ text: String(r.question), heading: HeadingLevel.TITLE })];13    for (const line of String(r.report_md ?? "").split("\n")) {14      const h = line.match(/^(#{1,4})\s+(.*)$/);15      if (h) paras.push(new Paragraph({ text: h[2], heading: [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3, HeadingLevel.HEADING_4][h[1].length - 1] }));16      else if (/^\s*[-*]\s+/.test(line)) paras.push(new Paragraph({ children: inline(line.replace(/^\s*[-*]\s+/, "")), bullet: { level: 0 } }));17      else if (line.trim()) paras.push(new Paragraph({ children: inline(line) }));18    }19    const doc = new Document({ sections: [{ children: paras }] });20    const buf = await Packer.toBuffer(doc);21    return new Response(new Uint8Array(buf), { headers: { "content-type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "content-disposition": `attachment; filename="${safe}.docx"` } });22  }23  return json({ run: r });24});2526function inline(text: string): (TextRun | ExternalHyperlink)[] {27  const out: (TextRun | ExternalHyperlink)[] = [];28  const re = /(\*\*[^*]+\*\*|\[[^\]]+\]\((https?:\/\/[^)]+)\)|https?:\/\/\S+)/g;29  let last = 0; let m: RegExpExecArray | null;30  while ((m = re.exec(text))) {31    if (m.index > last) out.push(new TextRun(text.slice(last, m.index)));32    const tok = m[0];33    if (tok.startsWith("**")) out.push(new TextRun({ text: tok.slice(2, -2), bold: true }));34    else if (tok.startsWith("[")) { const t = tok.match(/^\[([^\]]+)\]\((.*)\)$/)!; out.push(new ExternalHyperlink({ link: t[2], children: [new TextRun({ text: t[1], style: "Hyperlink" })] })); }35    else out.push(new ExternalHyperlink({ link: tok, children: [new TextRun({ text: tok, style: "Hyperlink" })] }));36    last = m.index + tok.length;37  }38  if (last < text.length) out.push(new TextRun(text.slice(last)));39  return out;40}4142export const DELETE = guarded<{ id: string }>(async (_req, { params }) => { await sql`DELETE FROM research_runs WHERE id = ${params.id}`; return json({ ok: true }); });43
Files · main
Menu

Theme